iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0

agent 寫的程式碼,跑起來就是你。2026-10-12,我請 agent 寫測試,它跑了一行 bun test,測試的清理程式把我家目錄裡的 .config、.local、.cache、.omp 刪了,沒有備份。那一行指令裡沒有一個危險的字:核准、allowlist、hook、規則,看的都是那一行字,刪檔卻發生在它啟動的行程裡。擋得住這種刪除的只有一樣東西:核心層級的沙箱,而且它得真的包住那一行。Cursor 在 Linux 上有這個沙箱,那天那個 shell 卻不在裡面;omp 原生沒有,但一支擴充能把每一個 bash 呼叫都包進去。事後要查也一樣:紀錄記下了那一行指令,沒記下它做了什麼,而且就放在被刪的那個家目錄裡;留下來的紀錄改了,兩邊的產品都看不出來。

Day28 處理的是「把 agent 開給別人叫」,最後收在一句:擋得住,不等於查得清。前面幾天談的,都是從外面來的東西:藏在內容裡的餌、別人寫的外掛、接進來的 MCP、叫它做事的外部程式。今天沒有攻擊者。agent 沒有被騙,它只是相信了一個自己沒驗證過的前提,寫了一段清理程式,然後照常跑測試。

這種事不是第一次。2025 年 7 月,Replit 的 agent 在程式凍結期間刪了正式資料庫(Day23 講過);2025 年 11 月底,Google Antigravity 為了清專案快取跑了 rmdir /s /q d:\,整個 D 槽沒了;2025 年 12 月,Claude Code 跑了 rm -rf tests/ patches/ plan/ ~/,最後那個 ~/ 把使用者的家目錄整個刪掉。那幾次,危險的字至少還寫在指令裡,看指令的人、比對指令的規則,還有機會攔下來。我這一次,指令是 bun test。

OWASP 的 Agentic 應用十大風險(2026 版)把這個形狀寫在 ASI02「Tool Misuse and Exploitation」:agent 在自己被授權的權限之內,把一個正當的工具用在不安全、非預期的地方,「for example deleting valuable data」;agent 自己生成、自己執行的程式碼,則是 ASI05「Unexpected Code Execution」。

今天就用這個事故回答兩個問題。一是擋得住嗎:agent 跑的程式碼在你的權限下動到家目錄,誰看得到、誰擋得住,擋不住的時候還剩什麼。二是查得清嗎:事後要說清楚誰要求、准了什麼、實際做了什麼,靠的是哪些紀錄,這些紀錄又改不改得動。

第二個問題,就是 Day17 說要留到今天談的「改不動的稽核紀錄」,也就是 Day28 結尾那句話裡的「查得清」。

另外先講清楚範圍:今天補上的防線只擋寫入,不擋讀取和網路,所以 agent 還是讀得到你的金鑰;要怎麼把金鑰搬到它碰不到的地方,Day16 講過。要連讀取和網路一起關,得換一種沙箱,第五節最後會介紹微軟開源的 MXC。agent 改寫自己的規則和記憶,留到 Day30。

一、情境:你請 agent 寫測試,它跑了一行 bun test

我在做 Day21 那個套件(擋住沒審過的 extension 的 omp-extension-guard),請 Cursor 的 agent 照規格寫,先寫測試。要測的東西會去讀「使用者層」的設定,像 ~/.omp/agent/extensions/、~/.config/ 底下的檔,所以測試得先造一個假的家目錄,把假的設定放進去,每個測試跑完再清掉。agent 是這樣寫的(節錄,原樣):

// test/setup.ts:測試的預載,每個測試檔之前跑
const home = fs.mkdtempSync(path.join(os.tmpdir(), "eg-home-"));
process.env.HOME = home;

// test/fixtures.ts:每個測試跑完(afterEach)呼叫
export function resetUserLevel(): void {
	for (const rel of [".omp", ".claude", ".codex", ".config", ".local", ".cache", ".aws", ".git-credentials", ".netrc", ".npmrc"]) {
		fs.rmSync(path.join(os.homedir(), rel), { recursive: true, force: true });
	}
}

讀起來很合理:先把 HOME 換成一個剛建的暫存目錄,之後所有路徑都從 os.homedir() 算,清理也只清「家目錄」底下那幾個資料夾。那個家目錄,應該是暫存的那個。

問題在一個前提:執行中改了 process.env.HOME,os.homedir() 會不會跟著變? Node 的文件寫,os.homedir() 在 POSIX 上「uses the $HOME environment variable if defined」。Bun 不會:它在行程啟動時就把家目錄定住了。我事後用一支只印 true/false 的探針重測(Bun 1.4.2):執行中把 HOME 換掉,process.env.HOME 是暫存目錄,os.homedir() 還是原本的家目錄;把同一行放進 bun test 的預載,測試裡讀到的也還是原本的家目錄;只有在行程啟動前就把 HOME 設好,os.homedir() 才是暫存目錄。

agent 其實問過自己這個問題。它在寫這段之前的紀錄裡寫著:執行中改 HOME,Bun 的 os.homedir() 到底會不會跟著變?接著用一句「我記得之前確認過,會」帶過,沒有跑一行程式去確認。Day20 那個套件裡就有一模一樣的預載,註解寫著這個測試套件「有自己的 HOME」;那一次沒出事,只因為它的清理只刪自己建的暫存目錄。這一次,多了一個刪 os.homedir() 底下東西的清理函式。

接著它在 ~/omp-extension-guard 裡跑測試:

timeout 180 bun test test/allowlist.test.ts test/inventory.test.ts 2>&1 | tail -60

這一行裡沒有 rm,沒有 ~,沒有任何一個家目錄裡的路徑。不管是你按下允許、比對 allowlist,還是交給 classifier 判斷,看到的都是這一行。

測試因為別的原因失敗了。agent 回頭除錯:使用者層的假設定怎麼找不到?它把 os.homedir() 印出來,是真的家目錄。再去看家目錄:.config、.local、.cache 不見了,.omp 被換成測試放進去、只剩 3 個項目的假版本。inventory 測試跑了兩次,每個測試結束,都清了一次「家目錄」。

被刪掉的,事後照之前 ls 的紀錄清點:

資料夾 裡面有什麼 刪掉之後
~/.omp omp 的設定(agent/config.yml,裡面設了自動載入 cursor-sdk 擴充)、存設定和憑證的 agent.db、sessions、logs、plugins omp 不再自動載入 cursor-sdk;設定、憑證、session 紀錄都沒了
~/.config 至少有 GitHub CLI 的登入(gh/hosts.yml) 登入沒了
~/.local 至少有 gh 本身,和 pi CLI 的安裝 git 透過 gh 拿 GitHub 憑證,git push 失敗;pi 指令不見了
~/.cache 各種快取 大多可以重建

清單裡另外幾個(.claude、.codex、.aws、.git-credentials、.netrc、.npmrc)這台機器上本來就沒有,算不上損失。所有程式庫、~/.ssh、~/.gitconfig、~/.bashrc 都還在。

接下來 agent 做的幾件事,後來都寫進了規則:停下來,什麼都不再寫,先唯讀地查還救不救得回來。沒有任何行程還開著被刪的檔(/proc/*/fd 裡沒有);檔案系統是 LVM 上的 ext4,沒有快照;sudo 要密碼,它不能、也不該自己動手做磁碟救援。它還發現,自己除錯時在真的 ~/.omp/agent/extensions/ 裡留下了一支測試用的 extension,真的 omp 下次啟動會自動載入它,於是只刪了那一個檔和它自己建的空資料夾。然後回報,讓我決定。我決定不救了,先把這次的教訓做成一道防線。

最後一點最值得記住:就算有人把那段測試從頭讀一遍,多半也會放行。 它先把 HOME 換成暫存目錄,之後才算路徑、才刪東西,看起來正是一段小心的程式。錯不在哪一行寫錯,而在一個關於執行環境的前提。讀程式碼的人、讀指令的規則,都看不到這種錯。

事後查得清嗎

Day28 給 omp 的最後一條建議是「記錄誰要求、准了什麼、工具是否真的執行」。拿這次事故來對:

要查的 有沒有紀錄 在哪裡
誰要求 有 Cursor 的 agent 對話紀錄:我請它照規格寫測試
准了什麼 有 同一份紀錄:那一行 bun test …
實際做了什麼 沒有 沒有任何紀錄記下 fs.rmSync 刪了哪些資料夾

我在 2026-10-12 數了一次這段對話的紀錄:654 次工具呼叫,工具結果 0 筆。它記下每一行指令,不記指令印了什麼,更不記指令啟動的行程寫了、刪了什麼。到底刪了什麼,是我拿事前和事後的 ls 比出來、再回頭讀 fixture 的程式碼推回去的。紀錄的盲點,和核准的盲點是同一個:都只看得到那一行字。

紀錄放在哪裡,是第二個問題。Cursor 的對話紀錄在 ~/.cursor/projects/ 底下,omp 的 session 和 log 在 ~/.omp 底下,都在家目錄裡。這次被刪的就有 ~/.omp:如果那天跑測試的是 omp,它自己那段 session 會跟著一起消失;Cursor 的紀錄留下來,只因為清理的清單裡剛好沒有 .cursor。2026 年 7 月 Claude Code 的 issue #74539 是另一種結局:整個家目錄被刪,連 ~/.claude 也在內,回報的人寫,那份 session 紀錄原本「would have identified the exact command」;現在只剩猜測,猜的正是兩種看不出來的刪法:一段執行時才組出路徑的清理指令,或是 classifier 沒看到內容的一支輔助腳本。

第三個問題,是留下來的紀錄改不改得動。這一點我之前(2026-10-09)就實測過,兩邊都一樣:同一個使用者跑的程式改得動,產品重新載入時看不出來。細節在下面兩節。

二、Cursor IDE 痛點:擋得住的只有沙箱,而那一行不在沙箱裡

先說清楚 Cursor 現在有什麼(2026-10-12 讀文件)。agent 跑終端機指令前要不要先問你,由 Run Modes 決定。3.6 版起預設是 Auto-review:allowlist 裡的直接跑,其他 shell 指令「run in the sandbox when possible」,進不了沙箱的交給一個 classifier 判斷;另外兩種是 Allowlist(沙箱可選)和 Run Everything(沒有沙箱,什麼都直接跑)。

沙箱在 Linux 上用 Landlock 和 seccomp,預設可以讀寫工作區和 /tmp,其他地方寫不進去。這正是擋得住這次刪除的那種東西:它是核心層級的限制,管的是行程實際做的事,不是指令長什麼樣子。bun test 如果在沙箱裡跑,測試裡的 fs.rmSync 要刪工作區外的 ~/.config,核心會直接拒絕。

其他每一層,看的都是那一行字,或 agent 自己的工具:

Cursor 的機制 它看的是什麼 擋得到 bun test 裡的 fs.rmSync 嗎
Allowlist 指令文字的前綴,大小寫有別;文件寫「Not a security boundary」 不能,那一行就是 bun test
Auto-review classifier 指令,必要時讀它要跑的腳本;文件寫「Auto-review is not a security boundary」 文件沒說它會讀到測試的 fixture;就算讀了,那段程式看起來是安全的
beforeShellExecution hook command、cwd、sandbox 三個欄位,只能 allow/deny/ask;出錯預設 fail-open 不能,沒有一個欄位碰得到行程裡做的事
File-Deletion/External-File Protection agent 自動刪檔(「including rm commands」)、在工作區外建改刪檔 文件沒寫涵不涵蓋指令啟動的行程
Checkpoints agent 改過的檔 文件沒寫涵蓋終端機或子行程的改動;被刪的又在工作區外
規則 給模型的指引;企業版文件寫「rules are suggestions, not guarantees」 不能,那是模型的自律
沙箱(Linux:Landlock+seccomp) 行程實際的寫入:工作區和 /tmp 以外寫不進去 能,只要那一行真的在沙箱裡跑

所以關鍵只剩一個問題:那一行,在不在沙箱裡?

這一次,它不在。 事後在同一個對話裡,從 agent 的終端機工具查它跑指令的 shell:文件說沙箱裡的行程會帶 CURSOR_SANDBOX=native,這個 shell 沒有;Landlock 和 seccomp 會留下的痕跡(seccomp 模式、no_new_privs),從這個 shell 一路往上查到 sshd,全都是 0。核心本身提供 Landlock ABI 8,不是核心太舊。

為什麼不在,文件給了兩個都對得上的理由,我沒辦法從紀錄確定是哪一個:

  • 要寫工作區以外的指令,進不了沙箱。 文件寫:「Commands that need more access, like full network access, writes outside the workspace, or privileged operations, can't use the sandbox, so they go to the classifier instead.」我的工作區是 ~/omp,測試卻在旁邊的 ~/omp-extension-guard 裡跑;對沙箱來說,那整個專案都在工作區外。在沙箱裡失敗的指令,也是「the agent can rerun it outside the sandbox. The classifier reviews that rerun.」
  • 這台機器少了沙箱要的那一塊。 Linux 沙箱需要非特權的 user namespace。文件寫,有些發行版用 AppArmor 限制 user namespace,「remote environments and the standalone CLI do not ship the profile」,要自己裝 Cursor 的 AppArmor 套件。這台正是 Remote SSH 連進去的 Ubuntu 26.04,開著那個限制(kernel.apparmor_restrict_unprivileged_userns = 1),而那個套件沒裝。

當時 Run Modes 選的是哪一種,我沒有記下來;Run Everything 本來就不用沙箱。不管是哪一個原因,結果都一樣:那一行帶著我完整的使用者權限在跑。

紀錄這一面

查的那一半,Cursor 這邊 2026-10-09 量過(Cursor 3.21.9、@cursor/sdk 1.0.32,只動測試自己造出來的紀錄,事後還原):

  • 這個工作區的對話紀錄有 159 份,沒有一份帶完整性欄位,工具結果是 0 筆。
  • 對話紀錄是衍生副本:把裡面的標記改掉 4 處,產品回放、resume 之後問它,答的都還是原值,因為回放根本不讀這份檔。它讀起來最像證據,卻連產品自己都不用。
  • 真正被回放的是 SDK 的 blob store,id 是內容的 sha256。把一個 blob 換成等長的假內容、id 不動,store 照收;之後讀取、resume,都沒有報任何完整性錯誤,也沒觀察到任何一步去重算雜湊。
  • hook 可以自己記一本帳,改了帳本也驗得出來;但把 hook 從設定裡拿掉,帳本 0 成長、沒有任何警告,hook 自己失敗時,工具照樣執行(fail-open)。

痛點是哪三件:

  1. 擋得住的那一層,有寫明的出口。 沙箱是唯一管得到行程實際動作的一層,但寫工作區以外的指令不進沙箱,沙箱裡失敗的可以改到沙箱外重跑,遠端環境缺了 AppArmor 套件就建不起來。agent 最常做的那種工作,在另一個資料夾裡做另一個專案,正好會走到出口。
  2. 其他每一層都只看那一行字。 Allowlist 比前綴,hook 拿到 command、cwd,classifier 判斷指令和它讀到的腳本,規則是模型的自律。bun test 這一行,哪一層都看不出它會刪家目錄。
  3. 刪了救不回來,也查不清楚。 Checkpoints 只還原 agent 改過的檔,文件沒說它管終端機或子行程的改動;Agent 安全的文件自己也寫「Changes save immediately to disk. Always use version control」。可是家目錄裡的設定和憑證,本來就沒人放進版本控制。要查的時候,對話紀錄只有指令、沒有結果,放在家目錄裡,改了產品也看不出來。

三、omp 如何平替:原生沒有沙箱,但擴充改得了「實際跑的那一行」

omp 原生的答案更直接:它沒有替 bash 準備沙箱。文件寫得很明白:「Pattern approval is not containment. Once approved, a process keeps the shell's ambient filesystem, network, and subprocess access.」核准決定一行指令跑不跑;跑起來之後,它就有你 shell 的全部權限。所以「原生的 omp 擋不擋得住這次的刪除」,答案很短:擋不住,它也從來沒說要擋。

但 omp 留了一個 Cursor 的 beforeShellExecution 沒有的點:擴充可以改掉實際要跑的那一行。 每一個工具呼叫執行前,omp 會先問擴充一次 tool_call;擴充可以擋,也可以回一份改過的輸入。文件寫,改過的輸入會重新驗證,之後的排程、事件、存檔、核准閘門,看到的都是改過的那份:你核准的,就是真的會跑的。

這就給了平替的路:一支擴充把每一個 bash 呼叫,改成「先把自己的寫入權限縮到專案和暫存目錄,再執行原本那一行」。Landlock 是 Linux 核心的功能,一般使用者就能用,不用 root:一個行程可以放棄自己在某些目錄以外的寫入權,之後它啟動的每一個行程都會繼承,而且收不回來。

Cursor(原生) omp(原生) omp(加一支擴充)
核心層級的寫入限制 有:Linux 用 Landlock+seccomp 沒有 有:每個 bash 呼叫都在 Landlock 底下
邊界畫在哪 工作區+/tmp —— 專案+暫存目錄+你列的地方,家目錄其餘部分一律保護
出口 寫工作區外的、沙箱裡失敗的,交給 classifier 或你決定在沙箱外跑 —— 沙箱跑不起來時,預設警告後只靠靜態檢查;可以設成整個擋下
hook 能不能改要跑的指令 beforeShellExecution 不能;另一支 preToolUse 文件寫可以回 updated_input tool_call 能,改過的會重新驗證、交給核准閘門 用的就是這一點

公平起見:Cursor 的 preToolUse 在文件上也能改寫工具輸入,同一招在 Cursor 理論上也做得到;這一篇只在 omp 上做了、測了。

四、解決前的 omp 能力:核准不是圍牆,靜態檢查看不進行程裡

我用預設什麼都沒加的 omp 18.2.8,在一個暫存的世界裡重演同一個形狀:家目錄裡放一個 precious/ 資料夾當 canary,專案裡放一支 cleanup.js,它和那次的 fixture 一樣,執行時才從 os.homedir() 算出要刪的路徑。照劇本回應的測試模型只叫一個工具:bash,指令是 bun cleanup.js。

結果:canary 被刪了,cleanup.js 還印出「cleaned the folder」。omp 照常核准、照常執行,它的 bash 工具沒有任何一步知道這支腳本要刪什麼。

靜態檢查也救不了這一場。所謂靜態檢查,是在 tool_call 那一刻讀每一個工具呼叫、算它要寫到哪裡。它讀得到 rm -rf ~/precious,擋得下來;它讀不到 bun cleanup.js 裡面那行 fs.rmSync。下一節那支 guard 的靜態層,就是照設計放過了這一行,交給沙箱。

紀錄這一面,omp 原生記得比較多:session 檔是一份 append-only 的 JSONL,每一個工具結果(toolResult)都會記進去(文件),bun cleanup.js 印出的那一行會留在紀錄裡。但它不記行程寫了、刪了哪些檔;它放在 ~/.omp,正是這次被刪的那一個;它也改得動。2026-10-09 對 session 檔的副本做四種竄改:改一個字、刪一行、調換順序、改完再把後面的雜湊連鎖重算,--resume 全部照吃、0 次警告;改一個字和重算那兩場,被改的那一句還被 agent 當成自己說過的話覆述出來。

把原生 omp 在這個情境的能力收成一張表:

原生 omp
rm -rf ~/precious 這種寫在指令上的刪除 bash.patterns 的核准規則擋得到,但文件自己說那是核准、不是隔離
bun cleanup.js 裡、執行時才算出路徑的刪除 擋不到,已實測:canary 被刪
指令啟動的子行程 繼承 shell 的全部權限
eval(omp 裡跑的 JS、omp 啟動的 Python kernel) 和 omp 一樣的權限,不在任何沙箱裡
擴充能不能改要跑的指令 能,tool_call 回一份新的輸入
事後查「實際做了什麼」 session 檔記下工具結果,不記行程寫了、刪了哪些檔
紀錄放在哪、改不改得動 ~/.omp,在家目錄裡;改了 resume 照吃、0 次警告(2026-10-09 實測)

方向很清楚,要補三件事:讀得懂的,在工具呼叫那一刻就擋,給模型一個清楚的理由;讀不懂的,交給核心:讓它跑,但讓它寫不進家目錄;紀錄要放在 agent 的程式碰不到的地方,而且改了要看得出來。

五、解決後的 omp 能力比較:補上兩層、守住紀錄,再跟 Cursor 並排

圖一:一個工具呼叫,在補上 guard 的 omp 裡經過哪兩層

https://ithelp.ithome.com.tw/upload/images/20261013/20183541pZO1rvCAMJ.png

靜態檢查在 omp 行程裡,讀得懂的直接擋,理由回給模型;bash 呼叫被改寫成先由 helper 用 Landlock 縮小寫入權、再換成原本那一行,所以它啟動的每一個行程都寫不進家目錄。核准閘門判的是改寫後的那一行。write、edit、eval 和 omp 自己寫的 session 紀錄都在沙箱外。紅色的格子是擋下來的結果。下面逐層說明。

補上的兩層,加一條給寫測試的 agent 的規矩

1. 靜態檢查(每個平台)。 我寫了一支擴充 omp-home-guard。tool_call 時算出 write、edit(包括改名、合併 patch 裡列出的檔)、ast_edit 會寫到哪裡,經過 ~、$HOME、..、符號連結都算,寫進受保護的地方就擋,並告訴模型哪裡可以寫。bash 讀得懂的部分:重新導向;rm、mv、cp、tee、dd、sed -i、find -delete、git -C、tar、curl -o 這類指令的目標;cd 進家目錄;從家目錄指派的變數;被家目錄餵資料的 xargs;bash -c、python -c、node -e 裡的內嵌程式碼。eval 的程式碼只要提到家目錄,又會寫檔、刪檔或開行程,就擋;提到家目錄的那一句即使在同一個 kernel 的前一次呼叫裡,也算,直到 kernel 被重設。

2. Landlock 沙箱(Linux)。 每一個 bash 呼叫都被改寫成:先啟動一支小 helper,它用 Landlock 把自己的寫入、建立、刪除、改名、截斷權限縮到可寫的地方(專案、暫存目錄、你的 allowWrite),再把自己換成 bash -c '<原本的指令>',在原本的目錄裡跑。輸出、結束碼都照舊;寫到別處,核心回 EACCES。helper 要是沒辦法先把自己限制住,就不跑原本的指令,直接回報失敗:結束碼 126,在 shell 的慣例裡就是「這個指令沒有被執行」。寧可不跑,也不在沒保護的情況下照跑。改寫時要避開幾個自己的坑:

  • helper 跑的是複本,放在 guard 自己在家目錄裡的狀態資料夾,受兩層保護;session 一開始先用 --probe 跑一次,真的限制住自己才算可用。
  • Bun 從一個空資料夾啟動 helper:Bun 會執行它啟動所在目錄的 bunfig.toml 預載、載入那裡的 .env,專案裡的這兩樣東西不能在限制生效前先跑。
  • 啟動 helper 時清空 BUN_OPTIONS:實測,裡面的 --preload 會比 helper 自己的程式更早跑;helper 再把原值還給你的指令。
  • 工具呼叫的 cwd 原封不動:omp 照常解析它、照常套用專案的 direnv,skill:// 這類內部網址也留在 omp 會展開的位置。

3. 給寫測試的 agent 的規矩。 guard 管的是 omp 的 agent,那次動手的卻是 Cursor 的 agent。所以另外做了兩樣東西。一條每個對話都套用的 Cursor 規則:不准從 os.homedir()、$HOME、~ 算出要刪或要寫的路徑;測試的家目錄要在行程啟動前就換好;每一次遞迴刪除,都要先確認目標在同一段程式自己建的暫存目錄裡;萬一刪到了,停下來、不再寫入、回報。另一樣是測試的 runner:它先建一個暫存根目錄,再帶著 HOME、TMPDIR 啟動 bun test;預載檢查 os.homedir() 真的在那個根目錄裡,不在就拒絕執行。規則是給模型的指引,Cursor 自己的文件也說規則不是保證;真正擋得住的是 runner 和預載。文末附了 runner 的程式。

真的跑一次

同一個暫存世界,加上 guard,照劇本跑四個工具呼叫:

  • session 一開始,通知寫著「bash runs in a Landlock sandbox (ABI 8)」,後面列出可寫的地方。
  • rm -rf ~/precious:靜態檢查直接擋下。模型收到的是「omp-home-guard blocked bash: it would change ~/precious, which is in your home directory outside the places this session may change (…)」。
  • bun cleanup.js:靜態檢查放行,沙箱擋下。模型收到的是 Bun 的錯誤 EACCES: permission denied, rm '<run>/home/precious',結束碼 1;canary 還在。
  • echo built > out.txt && cat out.txt:在專案裡照常寫、照常印出 built。
  • write 到 ~/.bashrc:擋下,檔案沒有被建出來。omp 的事件流裡,bash 工具收到的確實是改寫過的那一行。

另一場在沙箱裡跑 cat skill://demo/SKILL.md,以及在 cwd: "skill://demo" 底下跑 pwd,兩個都照常運作。不呼叫模型的測試有 76 條,全過(11 秒,不用網路),其中 9 條直接在真的 Landlock 上跑 helper:允許的地方寫得進去;rm -rf、覆寫、建立都被擋;bun -e 的 rmSync 和一支自己組路徑的腳本,都在子行程裡被擋;輸出、結束碼 5、工作目錄照舊;Landlock 最多疊 16 層,第 17 層的 helper 限制不了自己,就回報失敗(結束碼 126),指令沒跑。成本是每個 bash 呼叫多約 0.23 秒(20 次的中位數),大部分是 Bun 啟動的時間。

紀錄這一半:刪不掉,改了看得出來

沙箱順便守住了紀錄。它保護的是整個家目錄,~/.omp、~/.cursor 都在裡面,所以 agent 跑的指令和那些指令啟動的程式,刪不掉、也改不了 agent 自己的紀錄。這一點是從家目錄的測試推出來的,沒有對這兩個資料夾單獨跑。omp 自己寫 session 不受影響:寫的是 omp 的行程,它不在沙箱裡。

要讓紀錄改了看得出來,用的是一本雜湊鏈帳本,2026-10-09 實測過。一支擴充掛在 session_start、tool_call、tool_result、turn_end,每個事件往一本帳 append 一筆,每筆 h = sha256(上一筆的 h + 這一筆),鏈頭另外寫到 session 目錄以外的一個錨點;帳本只記事件種類、工具名和輸入的欄位名,不記內容。開場先自報到一筆,「沒掛 guard」和「掛了但沒事」分得出來。驗證器自測 12 種組合全部如預期:沒有鏈的純 JSONL,四種竄改都驗不出來;有鏈的本機驗法抓到改、刪、換;連雜湊一起重算的,只有對上錨點才抓得到。對 omp 那四種竄改,結果一樣。

補不上的也要講清楚:那次的錨點只是另一個暫存目錄,帳本和錨點都在同一台機器上,同一個使用者都改得動。真正改不動,要把鏈頭即時送到這台機器以外,這一步沒做。而且帳本記下的仍然是工具事件,不是行程寫了、刪了哪些檔;被沙箱擋下的寫入,只出現在指令的輸出裡(EACCES)。

補上前和補上後

原生 omp 補上之後 證據
rm -rf ~/precious 跑了就刪 靜態檢查擋下,告訴模型哪裡可以寫 已實測
bun cleanup.js(執行時才算路徑) canary 被刪 沙箱擋下(EACCES),canary 還在 已實測
專案裡的寫入 照常 照常 已實測
write ~/.bashrc 照寫 擋下 已實測
子行程 繼承全部權限 繼承 Landlock 的限制 已實測(bun -e、自己組路徑的腳本)
helper 自己壞掉 —— 不跑指令,回報失敗(結束碼 126) 已實測(第 17 層)
沙箱跑不起來(macOS、舊核心) —— 預設警告,靜態檢查變嚴格;設 onUnavailable: "block" 就整個擋 已實測(指定一個不存在的 Bun 來模擬)
每個 bash 呼叫的成本 —— 多約 0.23 秒 已實測(20 次中位數)
agent 的程式刪自己的紀錄(~/.omp、~/.cursor) 刪得掉,這次 ~/.omp 就沒了 沙箱擋住,家目錄整個受保護 推論(家目錄的測試)
紀錄被改 resume 照吃、0 次警告 掛雜湊鏈帳本:改、刪、換本機驗得出,重算靠錨點驗得出 已實測(2026-10-09)

跟 Cursor 並排看

補上之後的 omp Cursor IDE(文件+這次的觀察)
擋得住指令啟動的行程刪家目錄嗎 能,每個 bash 呼叫都在 Landlock 底下 在沙箱裡能;這次那一行不在沙箱裡
邊界畫在哪 家目錄除了專案、暫存目錄、allowWrite,其餘全部保護,不管專案放在哪 工作區+/tmp;要寫工作區外的指令不進沙箱
進不了沙箱時 你設定:警告,或整個擋 交給 classifier 或你;遠端缺 AppArmor 套件時沙箱建不起來
看得出那一行有沒有在沙箱裡嗎 session 通知寫明,每個 bash 呼叫都被改寫 文件說會標示沙箱外執行;hook 有 sandbox 欄位,沙箱裡有 CURSOR_SANDBOX
只看指令文字的那幾層 靜態檢查,讀得懂的直接擋 allowlist、hook、classifier、規則
紀錄記下「實際做了什麼」嗎 session 記工具結果,不記檔案操作 對話紀錄不記工具結果(0 筆),也不記檔案操作
紀錄改了看得出來嗎 原生看不出來;掛雜湊鏈帳本加錨點才驗得出 看不出來;hook 的帳本可以被靜默拿掉,失敗時放行

兩邊的差別不在誰有沙箱:Cursor 原生就有,omp 原生沒有。差別在邊界由誰畫、出口開在哪裡。Cursor 的邊界是工作區,出口留給 classifier 和你;補上之後的 omp,邊界是「家目錄裡除了這幾個地方都不准寫」,出口是沙箱跑不起來的時候:預設警告後只靠靜態檢查,要關死得自己設成 block。紀錄那一面,兩邊原生都一樣改了看不出來;omp 多的,是擴充能擋、能自報到,帳本可以接成一條鏈。

還沒補上的

限制 現在
讀取和網路 不管。家目錄裡的祕密讀得到,也送得出去;要關,得換成下面 MXC 這種預設全關的沙箱
權限、擁有者、時間戳 Landlock 管寫入、建立、刪除、改名、截斷,不管這些中繼資料;靜態檢查擋得到它讀得懂的 chmod、chown、touch
只有 bash 在沙箱裡 eval、omp 自己的寫入、其他擴充、MCP 工具、你自己的 ! 指令,都不在裡面
只有 Linux macOS、Windows 只有靜態檢查
shell 狀態 沙箱裡的 bash 呼叫之間,export 的變數、定義的函式不會留下來
真的模型、開著核准 實測用照劇本的測試模型、--approval-mode yolo;真的模型被擋之後怎麼反應、核准提示怎麼顯示改寫過的指令,都沒測
guard 本身 裝在 agent 寫得到的地方就可能被改,要裝在專案之外
紀錄的機外錨點 鏈頭沒有送出這台機器;帳本和錨點,同一個使用者都改得動
行程做了什麼 沒有一份紀錄記下成功或被擋下的檔案操作;被擋的寫入只在指令的輸出裡

再往上一層:MXC 把讀取、網路、環境變數也關起來

上面那張表的第一列,Landlock 這一層補不上:它管寫,不管讀取和網路。要連這些一起關,得換一種沙箱。微軟開源的 MXC(Microsoft eXecution Container,MIT 授權)就是這一類,它對自己的介紹是「a sandboxed code execution system for running untrusted code (model output, plugins, and tools)」。以下只讀了它的文件和原始碼(1.0.0,2026-10-12),沒有在這台機器上跑。

它有五個關鍵概念:

  • 一份規則,換平台換後端。 呼叫端用一份有版本的 JSON 寫清楚三件事:用哪種容器、規則是什麼、要跑什麼指令;MXC 驗過規則,再交給對應的後端。Linux 預設用 bubblewrap,要更強的隔離有 LXC、microVM、Hyperlight;macOS 用 Seatbelt,Windows 用 ProcessContainer。
  • 檔案系統預設全關。 bubblewrap 後端只把系統目錄唯讀掛進沙箱,其餘的主機路徑,包括「the caller's $HOME」,在沙箱裡都看不到:~/.ssh、~/.aws/credentials 連讀都讀不到。要用的路徑逐一列進唯讀或可寫;要遮掉的列進 deniedPaths,目錄用一個空的 tmpfs 蓋住,檔案用 /dev/null 蓋住。
  • 網路預設不通。 egress.default: "deny" 會給沙箱一個自己的網路 namespace,裡面只有它自己的 loopback。要放行特定位址,則改用一個接上 slirp4netns 的網路 namespace,在裡面用 iptables 寫規則;需要的工具少了一樣就直接失敗,文件寫明「neither ever falls back to sharing the host network namespace」。
  • 環境變數清空。 沙箱用 --clearenv 建起來,「so host secrets can't leak into untrusted code」:預設只給 PATH、TERM 這幾個基本變數,其餘要你明列。
  • 擋下來的,記下來。 預設全關的沙箱,最難的是寫規則時不知道它擋了什麼。MXC 在 Windows 上有「拒絕並記錄」的模式(learningModeLogging),照擋、照記。另一種是全部放行、全部記錄的稽核模式(permissiveLearningMode;Windows 執行器的 --audit 也是這一類),它會關掉防護,文件警告「Never use it to run untrusted code」。

圖二:MXC 的架構:一份請求,選一個後端,關進沙箱

https://ithelp.ithome.com.tw/upload/images/20261013/20183541VQGYumEL9k.png

上半是分層:不管從哪一種呼叫端進來,都是同一份請求,驗過之後由引擎依平台選後端。下半是 Linux 預設的 bubblewrap 後端在沙箱裡關掉的三樣東西。圖只畫文件和原始碼寫的,沒有在這台機器上跑;其他後端只標出名字。

代價也寫在文件裡:bubblewrap 後端要 bubblewrap 0.5 以上,還要非特權的 user namespace。這台機器兩樣都沒有:bubblewrap 沒裝,一般行程建 user namespace 會被拒(unshare --user 回「Operation not permitted」),正是第二節擋下 Cursor 沙箱的那個條件。Cursor 的 Linux 沙箱也是這個方向:網路預設擋,3.23 起還能把讀取限制在工作區;它和 MXC 一樣要用到 user namespace。Landlock 不用 namespace,所以在這台機器上,先補得上的是寫入這一層。

兩層可以疊起來用:Landlock 擋寫,namespace 沙箱再把讀取、網路、環境變數關起來。omp 這邊的接法和今天一樣:在同一個 tool_call 改寫點,把 bash 呼叫交給 MXC 的執行器(Linux 是 lxc-exec)或 Node SDK 的 spawn。這一步,這一篇沒有做,也沒有測。

可以先從哪裡做起?

第一步,知道家目錄裡哪些東西刪了就回不來。 不是程式碼,程式碼在 git 裡;是那些沒人放進版本控制的:CLI 的登入(gh、雲端的 CLI)、agent 自己的設定和憑證、本機裝的工具。列出來,備份起來。誰都該做到這一步。

第二步,讓測試碰不到真的家目錄。 agent 寫的測試要造假家目錄,就得在行程啟動前把 HOME 換好(runner 帶著環境變數啟動 bun test),預載再確認 os.homedir() 真的在暫存根目錄裡,不在就拒絕執行;每一次遞迴刪除,都先確認目標的真實路徑在同一段程式自己建的暫存目錄裡。會讓 agent 寫測試、跑測試的,做到這一步。

第三步,讓 agent 的 shell 真的進沙箱,而且確認它在裡面。 用 Cursor:Run Mode 用 Auto-review,或打開沙箱的 Allowlist;Linux 的遠端環境要裝 Cursor 的 AppArmor 套件;在 agent 的終端機裡看 CURSOR_SANDBOX 有沒有設;專案放在工作區裡,免得指令一開始就進不了沙箱。用 omp:裝 omp-home-guard 這類擴充,看 session 開頭的通知有沒有寫「Landlock sandbox」。讓 agent 自動跑指令的,一定要做到這一步。

第四步,把「進不了沙箱」當成要停下來的事。 核准、allowlist、classifier 看的是那一行字,只能當意圖檢查,不能當圍牆。沙箱跑不起來時,omp-home-guard 設 onUnavailable: "block";Cursor 這邊,沙箱外執行的提示不要順手按掉,安全用的 hook 設 failClosed: true。要讓它沒人看著也跑(排程、subagent,Day22、Day24),這一步少不了。

第五步,讓事後查得清。 先知道你的 agent 紀錄放在哪:Cursor 的對話紀錄、omp 的 session 都在家目錄裡,前者只記指令,後者記到工具結果,兩者都不記行程寫了、刪了什麼。要拿來當證據,紀錄得離開 agent 寫得到的地方,而且改了要看得出來:每個工具事件進一條雜湊鏈,鏈頭即時送到這台機器以外。要對別人交代 agent 做了什麼的,做到這一步。

留在 Cursor 的話:沙箱是你手上唯一管得到行程實際動作的東西。先確認它在你的機器上真的建得起來,再確認 agent 的工作落在工作區裡;其餘幾層,allowlist、hook、規則,都只看得到那一行字。事後要查,別只靠對話紀錄:它只有指令、沒有結果,改了產品也看不出來。

你可以怎麼確認自己讀懂了?

回到那一行 bun test,試著問自己四個問題。

第一題:那一行指令看一百遍,也看不出它會刪家目錄。為什麼?

因為刪檔不在指令裡,在指令啟動的程式裡;而且那段程式本身看起來也是安全的:它先把 HOME 換成暫存目錄,才去算路徑。錯的是一個關於執行環境的前提:Bun 在行程啟動時就定住了 os.homedir()。核准、allowlist、hook、classifier、規則,看的都是文字;這種錯不在文字裡。

第二題:測試明明先把 HOME 換成暫存目錄,為什麼刪到的還是真的?

因為換得太晚。預載在 Bun 啟動之後才跑,process.env.HOME 變了,os.homedir() 沒變(重測過的三種情況裡,只有「啟動前就設好」那一種,os.homedir() 才是暫存目錄)。所以修法不是換得早一點,而是讓 runner 帶著 HOME 去啟動 Bun,預載只負責確認、不負責換。再加一道:所有遞迴刪除,都先檢查真實路徑在自己建的暫存目錄裡。

第三題:Cursor 在 Linux 上明明有 Landlock 沙箱,為什麼這次沒擋下來?

因為那一行不在沙箱裡。事後查 agent 的 shell:沒有 CURSOR_SANDBOX,seccomp 和 no_new_privs 一路都是 0。文件寫的兩個出口,這次都對得上:要寫工作區以外的指令不進沙箱,而測試在工作區外的資料夾跑;遠端環境沒裝 Cursor 的 AppArmor 套件,Ubuntu 又限制了 user namespace。沙箱只保護真的在裡面跑的指令,所以要去確認,而且要讓「不在裡面」變成「停下來」,而不是「照跑」。

第四題:事後要查「實際做了什麼」,為什麼手上的紀錄幫不上忙?

因為紀錄記的也是那一行字。Cursor 的對話紀錄記下每一行指令,工具結果是 0 筆;omp 的 session 記到工具結果,一樣不記行程寫了、刪了哪些檔。紀錄又放在家目錄裡,和家目錄一起被刪的風險一樣;留下來的,同一個使用者改得動,兩邊產品重新載入時都看不出來(2026-10-09 實測)。要查得清,紀錄得放在 agent 碰不到的地方,改了要驗得出來,鏈頭還得離開這台機器。

給 agent 一個 shell,等於把你整個家目錄交給它寫的每一段程式。先決定它寫得到哪裡,再讓核心替你守住其餘的地方;要拿來查它的紀錄,也要放到它碰不到的地方。

明天 Day30 換一個方向:agent 不只會刪錯東西,還會改寫那些之後要拿來管它的規則和記憶。它改的那一份,跟後來真正生效的那一份,是不是同一份?

今天的 5 分鐘小練習

第 1 步:在一個新的暫存目錄裡跑一行 Bun,看 os.homedir() 會不會跟著改掉的 HOME。這一步什麼都不刪:

cd "$(mktemp -d)" && bun -e 'const os = require("os"); const before = os.homedir(); process.env.HOME = process.cwd(); console.log(os.homedir() === before)'

印出 true,代表 HOME 改了,os.homedir() 還是原本的家目錄。

第 2 步:請 agent 在它的終端機工具裡跑下面這一行,看它跑指令的 shell 在不在沙箱裡。在 Linux 上,Cursor 沙箱裡的行程 CURSOR_SANDBOX 會是 native;它的沙箱用 seccomp,所以 Seccomp 應該不是 0:

grep -E '^(NoNewPrivs|Seccomp):' /proc/self/status; echo "CURSOR_SANDBOX=${CURSOR_SANDBOX:-unset}"

第 3 步:用 omp-home-guard 的 helper 親眼看一次 Landlock 擋寫入。全程只動這一步自己建的暫存目錄,<omp-home-guard> 換成你 clone 下來的位置:

d=$(mktemp -d) && mkdir "$d/allowed" "$d/other" "$d/launch" && echo keep > "$d/other/keep.txt"
cd "$d/launch" && bun --no-env-file <omp-home-guard>/src/sandbox-exec.ts --cwd "$d/allowed" --allow "$d/allowed" -- "rm -f '$d/other/keep.txt'; echo ok > made-here.txt"
cat "$d/other/keep.txt"; ls "$d/allowed"; cd / && rm -rf "$d"

rm 會回「Permission denied」,keep.txt 還在;made-here.txt 則照常寫進允許的那個資料夾。

第 4 步:把家目錄裡刪了就回不來的東西列出來:登入、憑證、agent 的設定、本機裝的工具。每一樣問一次:備份在哪裡?如果明天它不見了,要多久才能恢復?


今天對應的威脅: T2(抄近路)和 T3(收不回的事)。這次沒有攻擊者:agent 用一句「我記得確認過」代替了驗證,這是 T2,它說的不等於它做的;結果是一個收不回的刪除,這是 T3,照 Day30 的說法:別人或它自己,驅動它去做收不回的事。OWASP Agentic Top 10 2026 的 ASI02「Tool Misuse and Exploitation」寫的正是這個形狀:「the agent operates within its authorized privileges but applies a legitimate tool in an unsafe or unintended way - for example deleting valuable data」;agent 自己生成、自己執行的程式碼,對應 ASI05「Unexpected Code Execution」,它的情境一就是 Replit:agent「generates and executes unreviewed install or shell commands in its own workspace, deleting or overwriting production data」。兩條的緩解都寫到沙箱:ASI02 的「Run tool or code execution in isolated sandboxes」,ASI05 的「Never run as root. Run code in sandboxed containers」。事後查證那一半,ASI02 的緩解第 8 條也寫了:「Maintain immutable logs of all tool invocations and parameter changes」;這次手上的紀錄,既不完整,也不是改不動的。本篇的實測沒有重演刪除:所有刪除都對著測試自己建的 canary,Bun 的探針不刪任何東西。

引用來源:

正文用事情本身來講,場次代號和檔案路徑收在這裡。以下路徑相對於 research_folder_omp_vs_Cursor/。

正文裡的那一段 紀錄
事故經過:預載和清理函式、agent 跑的指令、被刪的四個資料夾和裡面的東西、怎麼發現、事後查了什麼 那次 Cursor agent 對話的紀錄(2026-10-12);清點以事前的 ls 為準
agent 寫之前問過自己,用「我記得之前確認過」帶過 同一份紀錄
Day20 套件裡同樣的預載和註解 omp-skill-guard v0.1.0 的 test/setup.ts
Bun 的 os.homedir() 不跟著執行中的 HOME 變(三種情況) verification/day29-home-directory-deletion/homedir-probe.ts → homedir-probe-output.txt
agent 的 shell 不在 Cursor 的沙箱裡、這台機器少了的那一塊 verification/day29-home-directory-deletion/cursor-shell-status.sh → cursor-shell-status.txt
MXC 的前置條件:bubblewrap 沒裝、一般行程建 user namespace 被拒 verification/day29-home-directory-deletion/mxc-prereq-check.sh → mxc-prereq-check.txt
原生 omp:bun cleanup.js 刪掉 canary verification/home-guard/live-2/baseline/1-script-deletes-canary
加上 guard:rm -rf 被靜態擋、腳本被沙箱擋、專案寫入照常、~/.bashrc 被擋 verification/home-guard/live-2/guard/1-static-and-sandbox
沙箱下 skill:// 照常 verification/home-guard/live-2/guard/2-internal-urls
每個 bash 呼叫多約 0.23 秒 verification/home-guard/nfr4-latency.txt
76 條測試、真的 Landlock、第 17 層以 126 結束 home-guard/test/(bun run test)
這段對話的紀錄:654 次工具呼叫、0 筆工具結果 2026-10-12 數這次 Cursor agent 對話的紀錄
159 份對話紀錄、0 個完整性欄位、0 筆工具結果 verification/day29-home-directory-deletion/record-tamper/runs/no-model/inventory.json
對話紀錄是衍生副本:改 4 處,回放和 resume 仍是原值 verification/day29-home-directory-deletion/record-tamper/runs/S29-t1-jsonl
SDK blob 換成等長的假內容、id 不動:照收,讀取和 resume 0 次完整性錯誤 verification/day29-home-directory-deletion/record-tamper/runs/S29-t2-blob
hook 帳本:記 4 筆、改了驗得出、拿掉後 0 成長、失敗照跑 verification/day29-home-directory-deletion/record-tamper/runs/S29-hook
omp session 四種竄改,resume 0 次警告,兩場覆述被改的句子 verification/day29-home-directory-deletion/record-tamper/runs/O29-edit、O29-delete、O29-reorder、O29-recompute
雜湊鏈:開場自報到、自測 12 種組合如預期、四種竄改的驗法結果 verification/day29-home-directory-deletion/record-tamper/runs/O29-baseline、H29-chain-selftest;擴充 verification/day29-home-directory-deletion/record-tamper/hash-chain-hook.ts、驗證器 chain-verify.ts
圖一的每一格 home-guard/src/index.ts(session_start、tool_call、通知)、src/sandbox.ts(prepareSandbox、wrapCommand)、src/sandbox-exec.ts(以 126 結束、換成 bash -c);omp 的 extensibility/extensions/wrapper.ts:203-250(先問擴充,再過核准閘門)
圖二的每一格(只讀,沒有跑) mxc-main.zip 裡的 README.md(各平台後端)、docs/development/architecture/repository-architecture.md(分層、mxc_ffi、mxc_common、mxc_engine)、docs/backends/bwrap/bubblewrap-backend.md(沙箱裡關掉的三樣)、src/tools/{lxc,wxc,mxc_darwin}/Cargo.toml(執行器名稱)、sdk/node/src/native-library.ts(Node SDK 在行程裡載入 mxc_ffi)
兩張圖的 SVG、PNG day29-home-directory-deletion-fig1、-fig2 的 .svg 和 .png,由 verification/day29-home-directory-deletion/render-figs.mjs 從文中的 Mermaid 產生,結果在同一個資料夾的 render-figs.json

guard 在 home-guard/,另外發布成 omp-home-guard 0.1.0:src/index.ts(擴充本體)、src/policy.ts(靜態檢查)、src/sandbox.ts(改寫 bash 呼叫)、src/sandbox-exec.ts 和 src/landlock.ts(helper)。給 Cursor 的規則在 omp repo 根目錄的 .cursor/rules/home-directory-safety.mdc。故事裡那段預載和清理函式是 Day21 套件當時的版本,之後已改成文末附錄的寫法。紀錄那一半的實測,紀錄和程式都在 verification/day29-home-directory-deletion/record-tamper/。

實驗條件:live 每種情況跑一次。omp 18.2.8 從原始碼跑(bun packages/coding-agent/src/cli.ts),RPC 模式、--approval-mode yolo、關掉 LSP、rules、自動標題;模型那一步是 home-guard/test/scripted-provider.ts,照劇本回工具呼叫,並記下它收到的每一個請求,不是真的模型。每一場有自己的 HOME(裡面放 precious/keep.txt 當 canary)、TMPDIR、專案(放在那個 HOME 底下的 work/repo);guard 用 -e 載入,不讀設定檔。第一次 live 的 guard/2-internal-urls 有一個檢查寫錯(omp 會在 bash 結果後面加一行 Wall time,檢查拿整段結果去比),行為是對的;修好檢查後的重跑在 live-2/,第一次的紀錄留在 live/。測試和 live 都由 runner 帶著暫存的 HOME 啟動,跑前跑後比對我真正家目錄的清單,一模一樣。核心 Linux 7.0、Landlock ABI 8、Bun 1.4.2、x86_64。紀錄那一半是 2026-10-09 的實測:Cursor 3.21.9、@cursor/sdk 1.0.32、omp 18.2.8,模型 claude-opus-5-5;Cursor 的竄改都在隔離的 SDK 專案裡,omp 的竄改都在 session 檔的副本上,動前備份、動後逐位元組還原,每種情境跑一次。紀錄裡的路徑換成 <run>、<package>、<omp>、<bun>;掃過 email、金鑰、家目錄絕對路徑,都是 0。omp 的行號,docs/ 相對原始碼根目錄,其餘相對 packages/coding-agent/src/。

  • omp 18.2.8 文件
    • docs/bash-tool-runtime.md:46:「Pattern approval is not containment. Once approved, a process keeps the shell's ambient filesystem, network, and subprocess access.」
    • docs/approval-mode.md:72、docs/settings.md:182:bash.patterns 是核准規則,不是行程或檔案系統的隔離
    • docs/extensions.md:338:tool_call 可以擋,也可以改執行的 input;改過的會重新驗證,排程、事件、存檔、核准閘門看到的都是改過的
    • docs/extension-loading.md:273:「Extensions are not sandboxed (same process/runtime).」
    • docs/session.md:67(session 在執行時只追加)、173(toolResult:每一個工具結果的紀錄)、484(寫入沒有 fsync)
  • omp 18.2.8 原始碼(packages/coding-agent/src/ 底下)
    • extensibility/extensions/wrapper.ts:210-250(擴充回的 input 取代執行參數,再進核准閘門)、session/agent-session.ts:4166-4183
    • tools/bash.ts:845-856(沒給 cwd 時才把開頭的 cd 抽出來)、908(cwd 的解析)
    • tools/bash-skill-urls.ts:28-29、208-276、364-412(內部網址在較長的引號文字裡不展開;整段加引號的網址照樣展開)
    • exec/bash-executor.ts:116-146(direnv 依呼叫的目錄載入)
  • Cursor 官方文件(2026-10-12 讀取)
    • Run Modes:三種模式和沙箱欄;「run in the sandbox when possible」;「Commands that need more access, like full network access, writes outside the workspace, or privileged operations, can't use the sandbox, so they go to the classifier instead.」;「the agent can rerun it outside the sandbox. The classifier reviews that rerun.」;「Auto-review is not a security boundary」;Linux 用 Landlock 和 seccomp,要核心 6.2 以上、非特權 user namespace;「remote environments and the standalone CLI do not ship the profile」;CURSOR_SANDBOX(Linux 是 native);預設可寫工作區和 /tmp;File-Deletion、External-File Protection;更新紀錄 3.6「Auto-review shipped as the recommended default」
    • sandbox.json reference:type 預設 workspace_readwrite,insecure_none 整個關掉沙箱;additionalReadwritePaths
    • Permissions:allowlist 是前綴比對、大小寫有別;「Not a security boundary」
    • Hooks:beforeShellExecution 的輸入 command/cwd/sandbox,輸出 allow/deny/ask;出錯預設 fail-open,failClosed;preToolUse 的 updated_input
    • 企業 LLM 安全:「rules are suggestions, not guarantees. Combine rules with enforcement hooks」;「There is no security boundary between agents and your user account. If your account can delete files, agents can delete files (with approval by default).」
    • Agent overview:Checkpoints「Restoring a checkpoint reverts files only」、「Only use them for undoing Agent changes」
    • Agent 安全:「Changes save immediately to disk. Always use version control so you can revert changes.」
  • MXC(Microsoft eXecution Container)1.0.0 的文件與原始碼(github.com/microsoft/mxc,MIT;讀的是本機的 mxc-main.zip,sha256 開頭 0ae0d5ece2218577,2026-10-12)
    • README.md:定位(「a sandboxed code execution system for running untrusted code (model output, plugins, and tools)」)、各平台的預設與其他後端、呼叫端要指定的三件事(容器種類、規則、指令)、--audit「turns off all sandbox security」
    • docs/backends/bwrap/bubblewrap-backend.md:bubblewrap 0.5 以上與 user namespace 的前置條件;預設全關的檔案系統(「the caller's $HOME」看不到);deniedPaths 用 tmpfs 或 /dev/null 遮;egress.default: "deny" 用 --unshare-net,放行規則用 slirp4netns 加 iptables,缺工具就失敗;--clearenv
    • docs/logging-access-denied.md:learningModeLogging(照擋照記)與 permissiveLearningMode(全放全記),只在 Windows
  • Node.js 文件:os.homedir()(2026-10-12 讀取):「On POSIX, it uses the $HOME environment variable if defined.」這台機器沒裝 Node,未實測
  • 外部參考(2026-10-12 讀取)
    • OWASP Top 10 for Agentic Applications 2026:ASI02「Tool Misuse and Exploitation」的描述(「operates within its authorized privileges but applies a legitimate tool in an unsafe or unintended way - for example deleting valuable data」)和緩解第 3 條「Execution Sandboxes and Egress Controls」、第 8 條「Logging, Monitoring, and Drift Detection. Maintain immutable logs of all tool invocations and parameter changes.」;ASI05「Unexpected Code Execution (RCE)」的情境一(Replit)和緩解第 4 條「Execution environment security: Never run as root. Run code in sandboxed containers」
    • Claude Code:Docker,Coding Agent Horror Stories: The rm -rf ~/ Incident:2025-12-08 的 Reddit 貼文,rm -rf tests/ patches/ plan/ ~/;同文整理了 2025-10-21 的 GitHub issue #10077(權限系統開著,rm -rf 從根目錄刪起)和 2025-11-28 的 #12637(沒加引號的 rm -rf ~);Reddit 原貼
    • Claude Code:GitHub issue #74539(2026-07-05 開,之後因久未回應被自動關閉):Auto mode 下整個家目錄被刪,「~/.claude itself, which destroyed the session transcript that would have identified the exact command」;回報者自己猜的兩種機制:執行時才組出路徑的清理指令,或「a generated helper script whose contents were not what the classifier evaluated」
    • Google Antigravity:The Register(2025-12-01):Turbo 模式下清專案快取,跑了 rmdir /s /q d:\;AI Incident Database #1433,事件日期 2025-11-27
    • Replit:見 Day23 的引用來源(The Register,2025-07-21)
  • 本系列 Day16、Day17(把改不動的稽核紀錄留到今天)、Day20、Day21、Day22、Day23、Day24、Day28(「記錄誰要求、准了什麼、工具是否真的執行」)

附:測試的家目錄,要在啟動前就換好

這是事故之後 runner 和預載的寫法(取自 home-guard/test/run.ts、test/setup.ts、test/fixtures.ts,略去一部分)。重點是:runner 先建好暫存根目錄,再帶著 HOME 去啟動 bun test;預載不換 HOME,只確認 Bun 實際用的 os.homedir() 在那個根目錄裡;刪東西一律經過一個只准刪根目錄裡面的函式。

// test/run.ts:`bun run test` 跑的是這支,不是直接 `bun test`
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "hg-suite-")));
fs.mkdirSync(path.join(root, "home"));
fs.mkdirSync(path.join(root, "tmp"));
const env = { ...process.env, HOME: path.join(root, "home"), TMPDIR: path.join(root, "tmp"), HG_TEST_ROOT: root };
const child = Bun.spawn(["bun", "test", ...process.argv.slice(2)], { env, stdio: ["inherit", "inherit", "inherit"] });
const code = await child.exited;
fs.rmSync(root, { recursive: true, force: true });
process.exit(code);

// test/setup.ts:預載只檢查,不換
const root = process.env.HG_TEST_ROOT;
if (!root || !fs.existsSync(root) || !inside(os.homedir(), root) || !inside(os.tmpdir(), root)) {
	throw new Error(`Refusing to run: the home directory (${os.homedir()}) is not inside a temporary test root. Run the tests with \`bun run test\`.`);
}

// test/fixtures.ts:每一次遞迴刪除都經過這裡
export function safeRemove(target: string): void {
	let real: string;
	try {
		real = fs.realpathSync(target);
	} catch (err) {
		if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
		throw err;
	}
	const rel = path.relative(TEST_ROOT, real);
	if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`refusing to delete ${target}: outside the test root`);
	fs.rmSync(real, { recursive: true, force: true });
}

上一篇
把 agent 交給別人的程式:動作生效前,誰能喊停?
下一篇
它改寫自己的規則時,改的和生效的不是同一份
系列文
一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言